package com.xuecheng.media.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;
import com.xuecheng.base.exception.XueChengPlusException;
import com.xuecheng.base.model.PageParams;
import com.xuecheng.base.model.PageResult;
import com.xuecheng.base.model.RestResponse;
import com.xuecheng.media.mapper.MediaFilesMapper;
import com.xuecheng.media.mapper.MediaProcessMapper;
import com.xuecheng.media.model.dto.QueryMediaParamsDto;
import com.xuecheng.media.model.dto.UploadFileParamsDto;
import com.xuecheng.media.model.dto.UploadFileResultDto;
import com.xuecheng.media.model.po.MediaFiles;
import com.xuecheng.media.model.po.MediaProcess;
import com.xuecheng.media.service.MediaFileService;
import io.minio.*;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.*;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @description TODO
 * @author Mr.M
 * @date 2022/9/10 8:58
 * @version 1.0
 */
 @Service
 @Slf4j
public class MediaFileServiceImpl implements MediaFileService {

  @Autowired
  MediaFilesMapper mediaFilesMapper;
  @Autowired
  private MinioClient minioClient;
  @Autowired
  private MediaFileService currentProxy;
  @Autowired
  private MediaProcessMapper mediaProcessMapper;

  //普通文件桶
  @Value("${minio.bucket.files}")
  private String bucket_Files;

    //视频文件桶
    @Value("${minio.bucket.videofiles}")
    private String bucket_videoFiles;

    //媒资列表查询接口
 @Override
 public PageResult<MediaFiles> queryMediaFiels(Long companyId,PageParams pageParams, QueryMediaParamsDto queryMediaParamsDto) {

  //构建查询条件对象
  LambdaQueryWrapper<MediaFiles> queryWrapper = new LambdaQueryWrapper<>();

  queryWrapper.like(!StringUtils.isEmpty(queryMediaParamsDto.getFilename()), MediaFiles::getFilename, queryMediaParamsDto.getFilename());
  queryWrapper.eq(!StringUtils.isEmpty(queryMediaParamsDto.getFileType()), MediaFiles::getFileType, queryMediaParamsDto.getFileType());
  
  //分页对象
  Page<MediaFiles> page = new Page<>(pageParams.getPageNo(), pageParams.getPageSize());
  // 查询数据内容获得结果
  Page<MediaFiles> pageResult = mediaFilesMapper.selectPage(page, queryWrapper);
  // 获取数据列表
  List<MediaFiles> list = pageResult.getRecords();
  // 获取数据总数
  long total = pageResult.getTotal();
  // 构建结果集
  PageResult<MediaFiles> mediaListResult = new PageResult<>(list, total, pageParams.getPageNo(), pageParams.getPageSize());
  return mediaListResult;

 }


 private String getMimeType(String extension){
     if(extension == null){
         extension = "";
     }
     //根据扩展名取出mimeType
     ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
     String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;//通用mimeType，字节流
     if(extensionMatch!=null){
         mimeType = extensionMatch.getMimeType();
     }
    return mimeType;
 }
 public boolean addMediaFilesToMinIO(String localFilePath,String mimeType,String bucket, String objectName){

     try {
         UploadObjectArgs testbucket = UploadObjectArgs.builder()
                 //桶
                 .bucket(bucket)
                 //对象名 放在子目录下   objectName是日期+md5+拓展名
                 .object(objectName)
                 //指定本地文件路径
                 .filename(localFilePath)
                 //设置媒体文件的类型
                 .contentType(mimeType)
                 .build();
         minioClient.uploadObject(testbucket);
         log.debug("上传文件到minio成功,bucket:{},objectName:{}",bucket,objectName);
         System.out.println("上传成功");
         return true;
     } catch (Exception e) {
         e.printStackTrace();
         log.error("上传文件到minio出错,bucket:{},objectName:{},错误原因:{}",bucket,objectName,e.getMessage(),e);
         XueChengPlusException.cast("上传文件到文件系统失败");
     }
     return false;
 }

 private String getFileMd5(File file){
     try (FileInputStream fileInputStream = new FileInputStream(file)) {
         String fileMd5 = DigestUtils.md5Hex(fileInputStream);
         return fileMd5;
     } catch (Exception e) {
         e.printStackTrace();
         return null;
     }
 }
//获取文件默认存储目录路径 年/月/日
private String getDefaultFolderPath() {
     //创建了一个 SimpleDateFormat 对象 sdf，用于格式化日期。指定格式为"yyyy-MM-dd"，即年-月-日。
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
    //replace 方法将其中的"-"替换为"/"，并在末尾添加"/" 得到最终的文件夹路径字符串。
    String folder = sdf.format(new Date()).replace("-", "/")+"/";
    return folder;
}

 @Override
 public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, String localFilePath,String objectName) {
     String filename = uploadFileParamsDto.getFilename();
     String extention = filename.substring(filename.lastIndexOf("."));
     //根据拓展名判断文件的类型 是视频文件还是图片文件
     String mimeType = getMimeType(extention);

     String fileMd5 = getFileMd5(new File(localFilePath));
     ////获取文件默认存储目录路径 年/月/日
     String defaultFolderPath = getDefaultFolderPath();

     //现在上传的静态化页面时需要指定上传路径的  所以可以拓展原来的上传图片的方法 加一个参数 如果传来了路径则放到指定路径 如果没有则按年月日存储
     if(StringUtils.isEmpty(objectName)){
         //objectName其实就是数据库里面的file_path也就是除了桶的名字以外的完整路径 2022/09/20/1d0f0e6ed8a0c4a89bfd304b84599d9c.png
         //数据库中的url才是真正的访问路径 包括了桶的名字/mediafiles/2022/09/20/1d0f0e6ed8a0c4a89bfd304b84599d9c.png
         //然后在前端请求的完整地址为：http://192.168.101.65:9000/mediafiles/2022/09/20/1d0f0e6ed8a0c4a89bfd304b84599d9c.png
         objectName =  defaultFolderPath + fileMd5 + extention;
     }

     boolean result = addMediaFilesToMinIO(localFilePath, mimeType, bucket_Files, objectName);
     if(!result){
         XueChengPlusException.cast("文件上传后保存信息失败");
     }
     MediaFiles mediaFiles = currentProxy.addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_Files, objectName);
     if(mediaFiles == null){
         XueChengPlusException.cast("文件上传后保存信息失败");
     }

     UploadFileResultDto uploadFileResultDto = new UploadFileResultDto();
     BeanUtils.copyProperties(mediaFiles,uploadFileResultDto);
     return uploadFileResultDto;
 }
    //重写这个方法的目的就是进行复用  如果写在uploadFile其他地方用不到 因为把文件放到数据库里面的方法是一个常用的方法 需要把bucket写活
    //事务控制的条件是加上Transactional和调用这个方法是一个代理对象
    @Transactional
    public MediaFiles addMediaFilesToDb(Long companyId,String fileMd5,UploadFileParamsDto uploadFileParamsDto,String bucket,String objectName){
        //从数据库查询文件  判断如果存在则不进行插入操作
        MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
        if (mediaFiles == null) {
            //没有查到所以需要new一个
            mediaFiles = new MediaFiles();
            //拷贝基本信息
            BeanUtils.copyProperties(uploadFileParamsDto, mediaFiles);
            mediaFiles.setId(fileMd5);
            mediaFiles.setFileId(fileMd5);
            mediaFiles.setCompanyId(companyId);
            //桶名+objectName就是文件的访问路径
            mediaFiles.setUrl("/" + bucket + "/" + objectName);
            mediaFiles.setBucket(bucket);
            mediaFiles.setFilePath(objectName);
            mediaFiles.setCreateDate(LocalDateTime.now());
            mediaFiles.setAuditStatus("002003");
            mediaFiles.setStatus("1");
            //保存文件信息到文件表
            int insert = mediaFilesMapper.insert(mediaFiles);
//            int i = 1/0;
            if (insert < 0) {
                log.error("保存文件信息到数据库失败,{}",mediaFiles.toString());
                XueChengPlusException.cast("保存文件信息失败");
            }
            log.debug("保存文件信息到数据库成功,{}",mediaFiles.toString());

            //在文件上传后把上传的文件信息添加到mediaFiles表的同时把这个文件添加到mediaProcess 作为待处理任务 等待任务调度从里面取走任务
            addWaitingTask(mediaFiles);
        }
        return mediaFiles;

    }

    /**
     * 添加待处理任务
     * @param mediaFiles 媒资文件信息
     */
    private void addWaitingTask(MediaFiles mediaFiles){
        String filename = mediaFiles.getFilename();
        String extension = filename.substring(filename.lastIndexOf("."));
        String mimeType = getMimeType(extension);
        //"video/x-msvideo"它表示视频文件的内容类型 是固定的
        if(mimeType.equals("video/x-msvideo")){
            MediaProcess mediaProcess = new MediaProcess();
            BeanUtils.copyProperties(mediaFiles,mediaProcess);
            mediaProcess.setStatus("1");
            mediaProcess.setCreateDate(LocalDateTime.now());
            mediaProcess.setFailCount(0);
            //记录待处理的任务 一开始url要为空 因为这个url表示处理完视频后的url 不是原来的url 在任务处理完视频以后再把url放进去
            mediaProcess.setUrl(null);
            mediaProcessMapper.insert(mediaProcess);

        }
    }

    @Override
    //检查文件是否存在 首先检查数据库里面有没有 然后检查minio里面有没有 如果数据库有 minio没有这时候也需要上传文件
    public RestResponse<Boolean> checkFile(String fileMd5) {
        MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
        if(mediaFiles!=null){
            String bucket = mediaFiles.getBucket();
            String filePath = mediaFiles.getFilePath();
            GetObjectArgs getObjectArgs = GetObjectArgs.builder()
                    .bucket(bucket)
                    //object的参数并没有桶名 所以是filePath
                    .object(filePath)
                    .build();
            try {
                FilterInputStream inputStream = minioClient.getObject(getObjectArgs);
                if(inputStream!=null){
                    return RestResponse.success(true,"文件已经存在");
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return RestResponse.success(false);
    }

    @Override
    //猜测前端在进行文件上传的时候会先调用这个方法进行判断文件分块是否存在 存在则不传 不存在才传 就实现了断点续传
    //分块的信息在数据库是不保留的 只要查minio有没有这个分块 有的话断点续传则前面已经传过的就可以不传了
    public RestResponse<Boolean> checkChunk(String fileMd5, int chunkIndex) {
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
        GetObjectArgs getObjectArgs = GetObjectArgs.builder()
                .bucket(bucket_videoFiles)
                //object参数是具体到哪一个文件 而不是目录 不可再分
                .object(chunkFileFolderPath+chunkIndex)
                .build();
        try {
            FilterInputStream inputStream = minioClient.getObject(getObjectArgs);
            if(inputStream!=null){
                return RestResponse.success(true);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return RestResponse.success(false);
    }

    @Override
    //上传文件需要fileMd5值进行与chunk拼接成objectName  上传文件需要知道本地的文件路径 桶的名字 mimeType以及objectName
    public RestResponse uploadChunk(String fileMd5, int chunk, String localChunkFilePath) {
        //chunkFileFolderPath就是objectname 测试的时候是根据块的数量进行循环上传的（每一次循环上传一个分块文件 最终循环完了全部分块也上传完成）
        // 也就是前端也是循环传的chunk参数 如果在循环里面chunk检查存在则不传了实现了断点续传
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5) + chunk;
        //分块没有拓展名
        String mimeType = getMimeType(null);
                                                                            //chunkFileFolderPath就是objectname 没有拓展名
        boolean b = addMediaFilesToMinIO(localChunkFilePath, mimeType, bucket_videoFiles, chunkFileFolderPath);
        if(!b){
            return RestResponse.validfail(false,"上传分块文件失败");
        }
        return RestResponse.success(true);
    }

    @Override
    //合并文件首先需要知道分块文件所在的位置也就是chunkFileFolderPath 然后知道源文件的fileMd5和合并后的文件的fileMd5进行比较判断是否合并成功
    public RestResponse mergechunks(Long companyId, String fileMd5, int chunkTotal, UploadFileParamsDto uploadFileParamsDto) {
        //调用minioClient进行文件块的合并
        //chunkFileFolderPath表示存放chunk分块的目录
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
        String filename = uploadFileParamsDto.getFilename();
        String extension = filename.substring(filename.lastIndexOf("."));
        String objectName = getFilePathByMd5(fileMd5, extension);
        //sources表示指定源文件 且源文件需要进行过排序的 0 1 2 3进行排序然后在进行合并的
        //也是需要根据分块的数量进行循环  stream流进行循环以及映射 //每循环一次获得到一个分块文件 最终把所有的分块文件全部放到sources里面
        //使用 stream流进行iterate循环 从0开始循环 每循环一个进行+1 limit循环30次
        List<ComposeSource> sources = Stream.iterate(0,i->++i).limit(chunkTotal)
                //循环取出每一个分块的位置指定 在bucket里面的objectName进行循环取出 存到sources里面
                //Stream流map进行映射 也就是从这个映射里面取出分块存到sources里面
                .map(i->ComposeSource
                        .builder()
                        .bucket(bucket_videoFiles)
                        .object(chunkFileFolderPath+i)
                        .build())
                //把映射成的东西转成list放到source里面
                .collect(Collectors.toList());

        ComposeObjectArgs composeObjectArgs = ComposeObjectArgs.builder()
                .bucket(bucket_videoFiles)
                .sources(sources)
                //要合并成的视频文件的存放地方以及放进去的文件的名字
                .object(objectName)
                .build();
        try {
            minioClient.composeObject(composeObjectArgs);
        } catch (Exception e) {
            e.printStackTrace();
            log.error("合并文件出错，bucket:{},objectName:{},错误信息:{}",bucket_videoFiles,objectName,e.getMessage());
            return RestResponse.validfail(false,"合并文件异常");
        }
        //检查md5值判断文件是否上传成功  file就是合并后的文件
        File file = downloadFileFromMinIO(bucket_videoFiles, objectName);
        try ( FileInputStream fileInputStream = new FileInputStream(file)){
            //
            String mergeFile_md5 = DigestUtils.md5Hex(fileInputStream);
            if(!fileMd5.equals(mergeFile_md5)){
                log.error("校验合并md5值不一致，原始文件：{}，合并文件：{}",fileMd5,mergeFile_md5);
                return RestResponse.validfail(false,"文件校验失败");
            }

        } catch (Exception e) {
            return RestResponse.validfail(false,"文件校验失败");
        }
        //文件信息入库  这是视频文件的入库
        uploadFileParamsDto.setFileSize(file.length());
        MediaFiles mediaFiles = currentProxy.addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_videoFiles, objectName);
        if(mediaFiles ==null){
            return RestResponse.validfail(false,"文件入库失败");
        }
        //清理临时生成的文件块
        clearChunkFiles(chunkFileFolderPath,chunkTotal);

        return RestResponse.success(true);

    }

    @Override
    public MediaFiles getFileById(String mediaId) {
        MediaFiles mediaFiles = mediaFilesMapper.selectById(mediaId);

        return mediaFiles;
    }

    /**
     * 清除分块文件
     * @param chunkFileFolderPath 分块文件路径
     * @param chunkTotal 分块文件总数
     */
    private void clearChunkFiles(String chunkFileFolderPath,int chunkTotal){

        try {
            List<DeleteObject> deleteObjects = Stream.iterate(0, i -> ++i)
                    .limit(chunkTotal)
                    .map(i -> new DeleteObject(chunkFileFolderPath.concat(Integer.toString(i))))
                    .collect(Collectors.toList());

            RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs
                    .builder()
                    .bucket(bucket_videoFiles)
                    .objects(deleteObjects)
                    .build();

            Iterable<Result<DeleteError>> results = minioClient.removeObjects(removeObjectsArgs);
            results.forEach(r->{
                DeleteError deleteError = null;
                try {
                    deleteError = r.get();
                } catch (Exception e) {
                    e.printStackTrace();
                    log.error("清楚分块文件失败,objectname:{}",deleteError.objectName(),e);
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
            log.error("清楚分块文件失败,chunkFileFolderPath:{}",chunkFileFolderPath,e);
        }
    }

    /**
     * 从minio下载文件
     * @param bucket 桶
     * @param objectName 对象名称
     * @return 下载后的文件
     */
    public File downloadFileFromMinIO(String bucket,String objectName){
        //临时文件
        File minioFile = null;
        FileOutputStream outputStream = null;
        try{
            //getObject 方法获取指定对象（文件）的输入流（InputStream）这个输入流代表了从MinIO对象存储中读取的文件内容
            InputStream stream = minioClient.getObject(GetObjectArgs.builder()
                    .bucket(bucket)
                    .object(objectName)
                    .build());
            //创建临时文件
            minioFile=File.createTempFile("minio", ".merge");
            //创建临时文件的文件输出流
            outputStream = new FileOutputStream(minioFile);
            //通过文件输出流（FileOutputStream）将从MinIO获取的文件内容写入到临时文件中。
            //我们需要将这个文件内容写入到本地文件中，因此我们需要使用输出流（OutputStream）来实现写入操作
            //将从MinIO获取的输入流内容复制到本地文件的输出流中
            IOUtils.copy(stream,outputStream);
            //然后把本地文件放回回去
            return minioFile;
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            if(outputStream!=null){
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

    /**
     * 得到合并后的文件的地址
     * @param fileMd5 文件id即md5值
     * @param fileExt 文件扩展名
     * @return
     */
    //得到视频文件的存放路径 也就是获取到视频文件的objectname
    private String getFilePathByMd5(String fileMd5,String fileExt){
        //      "/"符号表示一层目录
        return   fileMd5.substring(0,1) + "/" + fileMd5.substring(1,2) + "/" + fileMd5 + "/" +fileMd5 +fileExt;
    }

    //得到分块文件的目录   得到分块的文件存放路径 是一个目录chunk 不是目录下的具体分块文件
    private String getChunkFileFolderPath(String fileMd5) {
        return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + "chunk" + "/";
    }

}
